using Jint.Native;
using Jint.Native.Iterator;

namespace Jint.Runtime.Interpreter.Expressions;

internal sealed class JintArrayExpression : JintExpression
{
    private JintExpression?[] _expressions = Array.Empty<JintExpression?>();
    private bool _hasSpreads;
    private bool _initialized;

    private JintArrayExpression(ArrayExpression expression) : base(expression)
    {
    }

    public static JintExpression Build(ArrayExpression expression)
    {
        return expression.Elements.Count == 0
            ? JintEmptyArrayExpression.Instance
            : new JintArrayExpression(expression);
    }

    private void Initialize()
    {
        ref readonly var elements = ref ((ArrayExpression) _expression).Elements;
        var expressions = _expressions = new JintExpression[((ArrayExpression) _expression).Elements.Count];
        for (var n = 0; n < expressions.Length; n++)
        {
            var expr = elements[n];
            if (expr != null)
            {
                var expression = Build(expr);
                expressions[n] = expression;
                _hasSpreads |= expr.Type == NodeType.SpreadElement;
            }
        }

        // we get called from nested spread expansion in call
        _initialized = true;
    }

    protected override object EvaluateInternal(EvaluationContext context)
    {
        if (!_initialized)
        {
            Initialize();
            _initialized = true;
        }

        var engine = context.Engine;
        var a = engine.Realm.Intrinsics.Array.ArrayCreate(_hasSpreads ? 0 : (uint) _expressions.Length);

        uint arrayIndexCounter = 0;
        foreach (var expr in _expressions)
        {
            if (expr == null)
            {
                a.SetIndexValue(arrayIndexCounter++, null, updateLength: false);
            }
            else if (_hasSpreads && expr is JintSpreadExpression jse)
            {
                jse.GetValueAndCheckIterator(context, out var objectInstance, out var iterator);
                // optimize for array
                if (objectInstance is JsArray ai)
                {
                    var length = ai.GetLength();
                    var newLength = arrayIndexCounter + length;
                    a.EnsureCapacity(newLength);
                    a.CopyValues(ai, sourceStartIndex: 0, targetStartIndex: arrayIndexCounter, length);
                    arrayIndexCounter += length;
                    a.SetLength(newLength);
                }
                else
                {
                    var protocol = new ArraySpreadProtocol(engine, a, iterator!, arrayIndexCounter);
                    protocol.Execute();
                    arrayIndexCounter += protocol._addedCount;
                }
            }
            else
            {
                var value = expr.GetValue(context);
                a.SetIndexValue(arrayIndexCounter++, value, updateLength: false);
            }
        }

        if (_hasSpreads)
        {
            a.SetLength(arrayIndexCounter);
        }

        return a;
    }

    private sealed class ArraySpreadProtocol : IteratorProtocol
    {
        private readonly JsArray _instance;
        private long _index;
        internal uint _addedCount;

        public ArraySpreadProtocol(
            Engine engine,
            JsArray instance,
            IteratorInstance iterator,
            long startIndex) : base(engine, iterator, 0)
        {
            _instance = instance;
            _index = startIndex - 1;
        }

        protected override void ProcessItem(JsValue[] arguments, JsValue currentValue)
        {
            _index++;
            _addedCount++;
            _instance.SetIndexValue((uint) _index, currentValue, updateLength: false);
        }
    }

    internal sealed class JintEmptyArrayExpression : JintExpression
    {
        public static JintEmptyArrayExpression Instance = new(new ArrayExpression(NodeList.From(Array.Empty<Expression?>())));

        private JintEmptyArrayExpression(Expression expression) : base(expression)
        {
        }

        protected override object EvaluateInternal(EvaluationContext context)
        {
            return new JsArray(context.Engine, Array.Empty<JsValue>());
        }

        public override JsValue GetValue(EvaluationContext context)
        {
            return new JsArray(context.Engine, Array.Empty<JsValue>());
        }
    }
}
